package org.haptimap.hcimodules.tactilecompass;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;

/**
 * The heading module computes the user's heading. If the user points the
 * device, the pointing direction will be considered as heading. If the user is
 * not pointing the device but walks, the walking direction extracted from the
 * GPS signal will be used as heading.
 * @author <a href="mailto:"martin.pielot@offis.de">Martin Pielot</a>
 * @version 06.12.2010
 */
public class HeadingModule implements LocationListener, SensorEventListener {

	// ========================================================================
	// Constant Fields
	// ========================================================================

	protected static final String	TAG							= "HeadingModule";

	protected static final int		MIN_WALKING_GPS_ACCURACY	= 50;
	protected static final int		MIN_POINTING_GPS_ACCURACY	= 100;

	public static int				use_compass_thresh			= 30;
	public static double			minSpeedForGpsHeading		= 1.0;

	// ========================================================================
	// Fields
	// ========================================================================

	private HeadingModuleListener	listener;

	private LocationManager			locationManager;
	private SensorManager			sensorManager;
	private Sensor					orientationSensor;

	private Location				userLocation;
	private int						orientationAccuracy;
	private int						pointingDirection;
	private boolean					usePointingDirection;
	private int						userHeading;

	// ========================================================================
	// Constructor
	// ========================================================================

	public HeadingModule(Context context, HeadingModuleListener listener) {
		this.listener = listener;

		this.locationManager = (LocationManager) context
				.getSystemService(Context.LOCATION_SERVICE);
		this.sensorManager = (SensorManager) context
				.getSystemService(Context.SENSOR_SERVICE);
		this.orientationSensor = sensorManager
				.getDefaultSensor(Sensor.TYPE_ORIENTATION);
	}

	public static HeadingModule registerHeadingModule(Context context,
			HeadingModuleListener listener) {
		return new HeadingModule(context, listener);
	}

	// ========================================================================
	// Methods
	// ========================================================================

	public void onStart() {
		Log.i(TAG, "onStart()");

		this.locationManager.requestLocationUpdates(
				LocationManager.GPS_PROVIDER,
				200, // min time in ms
				0, // min distance in m
				this);

		this.locationManager.requestLocationUpdates(
				LocationManager.NETWORK_PROVIDER,
				200, // min time in ms
				0, // min distance in m
				this);

		this.userLocation = locationManager
				.getLastKnownLocation(LocationManager.GPS_PROVIDER);

		this.sensorManager.registerListener(
				this,
				orientationSensor,
				SensorManager.SENSOR_DELAY_UI);
	}

	public void onStop() {
		Log.i(TAG, "onStop()");

		this.locationManager.removeUpdates(this);
		this.sensorManager.unregisterListener(this);

	}

	/**
	 * Returns whether the user is pointing the device(true) or having it in the
	 * pocket (false).
	 */
	public boolean isUsingPointingDirection() {
		return usePointingDirection;
	}

	/**
	 * Provides the user location for the listener module.
	 */
	public Location getUserLocation() {
		return userLocation;
	}

	/**
	 * Returns if the sensory data is valid = the heading is reliable.
	 */
	public boolean hasValidData() {

		// Data is not sufficient

		// if no user GPS location is available
		if (userLocation == null)
			return false;

		// if the user is pointing
		if (usePointingDirection) {

			// .. and the GPS location is not accurate enough
			if (userLocation.getAccuracy() > MIN_POINTING_GPS_ACCURACY) {
				return false;
			}

			// .. or the compass is not accurate enough
			if (orientationAccuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW) {
				return false;
			}

			// if the user is not pointing
		} else {

			// .. and the GPS location is not accurate enough
			if (userLocation.getAccuracy() > MIN_WALKING_GPS_ACCURACY) {
				return false;
			}

			// .. or the user is not walking fast enough
			if (userLocation.getSpeed() < minSpeedForGpsHeading) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Determines whether the user is standing still and not pointing the
	 * device. In this case the heading is not valid.
	 */
	public boolean isStanding() {

		// User is NOT standing

		// if no user GPS location is available
		if (userLocation == null)
			return false;

		// if the user is pointing
		if (usePointingDirection)
			return false;

		// if the GPS location is not accurate enough
		if (userLocation.getAccuracy() > MIN_WALKING_GPS_ACCURACY) {
			return false;
		}

		// if the user is walking too fast
		if (userLocation.getSpeed() >= minSpeedForGpsHeading)
			return false;

		// otherwise the user is standing
		return true;
	}

	// ------------------------------------------------------------------------
	// Implementation of 'LocationListener'
	// ------------------------------------------------------------------------

	public void onLocationChanged(Location location) {
		Log.d(TAG, "onLocationChanged( " + location + " )");

		// TODO this will mix WLAN and GPS positions. Use intelligent version
		// from PocketNavigator
		this.userLocation = location;
		this.updateHeading();
	}

	public void onProviderDisabled(String provider) {
	}

	public void onProviderEnabled(String provider) {
	}

	public void onStatusChanged(String provider, int status, Bundle extras) {
	}

	// ------------------------------------------------------------------------
	// Implementation of 'SensorEventListener'
	// ------------------------------------------------------------------------

	public void onSensorChanged(SensorEvent event) {

		if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {

			final float yaw = event.values[0];
			final float pitch = event.values[1];
			final float roll = event.values[2];

			this.orientationAccuracy = event.accuracy;
			this.pointingDirection = (int) (yaw + 0.5);

			// read pitch and roll to see if the user is pointing the device
			boolean usePointing = (Math.abs(pitch) < use_compass_thresh && Math
					.abs(roll) < use_compass_thresh);
			boolean notifyListeners = usePointing && !this.usePointingDirection;
			this.usePointingDirection = usePointing;

			this.updateHeading();

			if (notifyListeners)
				this.listener.onStartPointing();

		}
	}

	public void onAccuracyChanged(Sensor sensor, int accuracy) {
	}

	// ------------------------------------------------------------------------
	// Helpers
	// ------------------------------------------------------------------------

	private void updateHeading() {

		if (userLocation == null)
			return;

		boolean sufficientData = hasValidData();

		double heading = (int) (usePointingDirection
				? pointingDirection
				: userLocation.getBearing());

		if (userHeading != heading) {
			this.userHeading = (int) heading;
			this.listener.onHeadingChanged(heading, sufficientData);
		}

	}

}
